All files / web/src/app/api/curriculum/[playerId]/sessions/history route.ts

0% Statements 0/122
0% Branches 0/1
0% Functions 0/1
0% Lines 0/122

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                                                                                                                                                                                                                                                     
/**
 * API route for paginated session history with cursor-based pagination
 *
 * GET /api/curriculum/[playerId]/sessions/history
 *   Query params:
 *   - cursor: Session ID to start after (optional, for pagination)
 *   - limit: Number of sessions to return (default: 20, max: 100)
 *
 * Response:
 *   {
 *     sessions: PracticeSession[],
 *     nextCursor: string | null,  // ID of last session, null if no more
 *     hasMore: boolean
 *   }
 */

import { NextResponse } from 'next/server'
import { and, desc, eq, lt } from 'drizzle-orm'
import { db } from '@/db'
import { sessionPlans } from '@/db/schema/session-plans'
import { withAuth } from '@/lib/auth/withAuth'
import { canPerformAction } from '@/lib/classroom'
import { getUserId } from '@/lib/viewer'

export const GET = withAuth(async (request, { params }) => {
  const routeStart = performance.now()
  const timings: Record<string, number> = {}

  try {
    const { playerId } = (await params) as { playerId: string }
    const { searchParams } = new URL(request.url)

    const cursor = searchParams.get('cursor')
    const limitParam = searchParams.get('limit')
    const limit = Math.min(Math.max(parseInt(limitParam ?? '20', 10) || 20, 1), 100)

    if (!playerId) {
      return NextResponse.json({ error: 'Player ID required' }, { status: 400 })
    }

    // Authorization check
    let t = performance.now()
    const userId = await getUserId()
    const canView = await canPerformAction(userId, playerId, 'view')
    timings.auth = performance.now() - t

    if (!canView) {
      return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
    }

    // Build query conditions
    const conditions = [
      eq(sessionPlans.playerId, playerId),
      // Only include completed sessions
      // completedAt is not null check done via ordering
    ]

    // If cursor provided, get sessions older than the cursor session
    if (cursor) {
      t = performance.now()
      // Get the cursor session's completedAt to paginate from there
      const cursorSession = await db.query.sessionPlans.findFirst({
        where: eq(sessionPlans.id, cursor),
        columns: { completedAt: true },
      })
      timings.cursorLookup = performance.now() - t

      if (cursorSession?.completedAt) {
        conditions.push(lt(sessionPlans.completedAt, cursorSession.completedAt))
      }
    }

    // Fetch limit + 1 to check if there are more
    t = performance.now()
    const sessions = await db.query.sessionPlans.findMany({
      where: and(...conditions),
      orderBy: [desc(sessionPlans.completedAt)],
      limit: limit + 1,
    })
    timings.dbQuery = performance.now() - t

    // Filter to only completed sessions and check for more
    t = performance.now()
    const completedSessions = sessions.filter((s) => s.completedAt !== null)
    const hasMore = completedSessions.length > limit
    const returnSessions = completedSessions.slice(0, limit)

    // Transform to match PracticeSession interface expected by client
    const transformedSessions = returnSessions.map((session) => {
      const results = session.results
      return {
        id: session.id,
        playerId: session.playerId,
        startedAt: session.startedAt,
        completedAt: session.completedAt,
        problemsAttempted: results.length,
        problemsCorrect: results.filter((r) => r.isCorrect).length,
        totalTimeMs: results.reduce((sum, r) => sum + (r.responseTimeMs ?? 0), 0),
        skillsUsed: [...new Set(results.flatMap((r) => r.skillsExercised ?? []))],
      }
    })
    timings.transform = performance.now() - t

    const total = performance.now() - routeStart
    console.log(
      `[PERF] /api/curriculum/.../sessions/history: ${total.toFixed(1)}ms | ` +
        `auth=${timings.auth.toFixed(1)}ms, ` +
        `db=${timings.dbQuery.toFixed(1)}ms, ` +
        `transform=${timings.transform.toFixed(1)}ms | ` +
        `sessions=${returnSessions.length}`
    )

    return NextResponse.json({
      sessions: transformedSessions,
      nextCursor: hasMore ? returnSessions[returnSessions.length - 1]?.id : null,
      hasMore,
    })
  } catch (error) {
    console.error('Error fetching session history:', error)
    return NextResponse.json({ error: 'Failed to fetch session history' }, { status: 500 })
  }
})